using System;
using System.Runtime.InteropServices;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;

namespace SmallVCM
{
    public abstract class AbstractLight
    {

        /* \brief Illuminates a given point in the scene.
         *
         * Given a point and two random samples (e.g., for position on area lights),
         * this method returns direction from point to light, distance,
         * pdf of having chosen this direction (e.g., 1 / area).
         * Optionally also returns pdf of emitting particle in this direction,
         * and cosine from lights normal (helps with PDF of hitting the light,
         * but set to 1 for point lights).
         *
         * Returns radiance.
         */

        public abstract RgbSpectrum Illuminate(
            ref SceneSphere aSceneSphere,
            ref Vector aReceivingPosition,
            ref UV aRndTuple,
            out Vector oDirectionToLight,
            out float oDistance,
            out float oDirectPdfW,
            out float oEmissionPdfW,
            out float oCosAtLight);

        /* \brief Emits particle from the light.
         *
         * Given two sets of random numbers (e.g., position and direction on area light),
         * this method generates a position and direction for light particle, along
         * with the pdf.
         *
         * Can also supply pdf (w.r.t. area) of choosing this position when calling
         * Illuminate. Also provides cosine on the light (this is 1 for point lights etc.).
         *
         * Returns "energy" that particle carries
         */

        public abstract RgbSpectrum Emit(
            ref SceneSphere aSceneSphere,
            ref UV aDirRndTuple,
            ref UV aPosRndTuple,
            out Vector oPosition,
            out Vector oDirection,
            out float oEmissionPdfW,
            out float oDirectPdfA,
            out float oCosThetaLight);

        /* \brief Returns radiance for ray randomly hitting the light
         *
         * Given ray direction and hitpoint, it returns radiance.
         * Can also provide area pdf of sampling hitpoint in Illuminate,
         * and of emitting particle along the ray (in opposite direction).
         */

        public abstract RgbSpectrum GetRadiance(
            ref SceneSphere aSceneSphere,
            ref Vector aRayDirection,
            ref Vector aHitPoint,
            out float oDirectPdfA,
            out float oEmissionPdfW);

        // Whether the light has a finite extent (area, point) or not (directional, env. map)
        public abstract bool IsFinite();

        // Whether the light has delta function (point, directional) or not (area)
        public abstract bool IsDelta();
    }


    public class AreaLight : AbstractLight
    {
        public
            AreaLight(
            ref Vector aP0,
            ref Vector aP1,
            ref Vector aP2)
        {
            p0 = aP0;
            e1 = aP1 - aP0;
            e2 = aP2 - aP0;

            Vector normal = Vector.Cross(ref e1, ref e2);
            float len = normal.Length;
            mInvArea = 2f/len;
            mFrame.SetFromZ(ref normal);
        }

        public override RgbSpectrum Illuminate(
            ref SceneSphere aSceneSphere,
            ref Vector aReceivingPosition,
            ref UV aRndTuple,
            out Vector oDirectionToLight,
            out float oDistance,
            out float oDirectPdfW,
            out float oEmissionPdfW,
            out float oCosAtLight)
        {
            UV uv = new UV();
            MC.UniformSampleTriangle(aRndTuple.U, aRndTuple.V, ref uv.U, ref uv.V);

            Vector lightPoint = p0 + e1*uv.U + e2*uv.V;

            oDirectionToLight = lightPoint - aReceivingPosition;
            float distSqr = oDirectionToLight.Length;
            oDistance = MathLab.Sqrt(distSqr);
            oDirectionToLight = oDirectionToLight/oDistance;
            var invDir = -oDirectionToLight;

            var norm = mFrame.Normal();
            float cosNormalDir = Vector.Dot(ref norm, ref invDir);

            // too close to, or under, tangent
            if (cosNormalDir < Consts.EPS_COSINE)
            {
                oCosAtLight = float.MinValue;
                oDirectPdfW = 0f;
                oEmissionPdfW = 0f;
                return RgbSpectrum.ZeroSpectrum();
            }

            oDirectPdfW = mInvArea*distSqr/cosNormalDir;

                oCosAtLight = cosNormalDir;

                oEmissionPdfW = mInvArea*cosNormalDir*MathLab.INVPI;

            return mIntensity;
        }

        public override RgbSpectrum Emit(
            ref SceneSphere aSceneSphere,
            ref UV aDirRndTuple,
            ref UV aPosRndTuple,
            out Vector oPosition,
            out Vector oDirection,
            out float oEmissionPdfW,
            out float oDirectPdfA,
            out float oCosThetaLight)
        {
            UV uv = new UV();
            MC.UniformSampleTriangle(aPosRndTuple.U, aPosRndTuple.V, ref uv.U, ref uv.V);

            oPosition = p0 + e1*uv.U + e2*uv.V;

            Vector localDirOut = Utils.SampleCosHemisphereW(aDirRndTuple, out oEmissionPdfW);

            oEmissionPdfW *= mInvArea;

            // cannot really not emit the particle, so just bias it to the correct angle
            localDirOut.z = Math.Max(localDirOut.z, Consts.EPS_COSINE);
            oDirection = mFrame.ToWorld(ref localDirOut);

            oDirectPdfA = mInvArea;

            oCosThetaLight = localDirOut.z;

            return mIntensity*localDirOut.z;
        }

        public override RgbSpectrum GetRadiance(
            ref SceneSphere aSceneSphere,
            ref Vector aRayDirection,
            ref Vector aHitPoint,
            out float oDirectPdfA,
            out float oEmissionPdfW)
        {
            var n = mFrame.Normal();
            var wo = -aRayDirection;
            float cosOutL = Math.Max(0f, Vector.Dot(ref n, ref wo));

            if (Math.Abs(cosOutL) < MathLab.Epsilon)
            {
                oDirectPdfA = 0f;
                oEmissionPdfW = 0f;
                return RgbSpectrum.ZeroSpectrum();
            }

            oDirectPdfA = mInvArea;


            oEmissionPdfW = Utils.CosHemispherePdfW((Normal) mFrame.Normal(), -aRayDirection);
            oEmissionPdfW *= mInvArea;

            return mIntensity;
        }

        // Whether the light has a finite extent (area, point) or not (directional, env. map)
        public override bool IsFinite()
        {
            return true;
        }

        // Whether the light has delta function (point, directional) or not (area)
        public override bool IsDelta()
        {
            return false;
        }



        public Vector p0, e1, e2;
        public ONB mFrame;
        public Vector mIntensity;
        public float mInvArea;
    };


    public class PointLight : AbstractLight
    {
        public override RgbSpectrum Illuminate(ref SceneSphere aSceneSphere, ref Vector aReceivingPosition, ref UV aRndTuple, out Vector oDirectionToLight, out float oDistance, out float oDirectPdfW, out float oEmissionPdfW, out float oCosAtLight)
        {
            throw new NotImplementedException();
        }

        public override RgbSpectrum Emit(ref SceneSphere aSceneSphere, ref UV aDirRndTuple, ref UV aPosRndTuple, out Vector oPosition, out Vector oDirection, out float oEmissionPdfW, out float oDirectPdfA, out float oCosThetaLight)
        {
            throw new NotImplementedException();
        }

        public override RgbSpectrum GetRadiance(ref SceneSphere aSceneSphere, ref Vector aRayDirection, ref Vector aHitPoint, out float oDirectPdfA, out float oEmissionPdfW)
        {
            throw new NotImplementedException();
        }

        public override bool IsFinite()
        {
            throw new NotImplementedException();
        }

        public override bool IsDelta()
        {
            throw new NotImplementedException();
        }
    }


}